
program Eschecs;

{.$DEFINE REDIRECT_STDERR}
{$DEFINE USE_STYLE}
{.$DEFINE DEBUG_OUTPUT}
{.$DEFINE USE_LANGUAGE_UNIT}

uses
{$IFDEF UNIX}
  CThreads,
  CWString,
  BaseUnix,
{$ENDIF}
  Classes,
  Math,
  RegExpr,
  SysUtils,
  StrUtils,
  TypInfo,
{$IFDEF WINDOWS}
  StreamIo,
{$ENDIF}
  
  fpg_base,
  fpg_cmdlineparams,
  fpg_constants,
  fpg_dialogs,
  fpg_form,
  fpg_main,
  fpg_menu,
  fpg_panel,
  fpg_stylemanager,
  fpg_widget,
  
  BGRABitmap,
  BGRABitmapTypes,
  
  ChessGame,
  ChessTypes,
  Eco,
  Fen,
  FenFilter,
  PgnWrite,
  Uci,
  
  BGRAChessboard,
  Images,
  
  Book,
  Connect,
  Engines,
  Language,
  MoveString,
  Permission,
  Settings,
  Sound,
  Utils,

{$IFDEF USE_STYLE}
  FormStyle,
{$ENDIF}
  FormAbout,
  FormDebug,
  FormPromotion,
  
  Constants;

{$IFDEF WINDOWS}
{$R eschecs.res}
{$ENDIF}

const
  CVersion = {$I version};

type
  TNavigation = (nvPrevious, nvNext, nvLast, nvFirst);

  TListener = class(TThread)
  private
    FMessage: string;
    procedure OnEngineMessage;
  protected
    procedure Execute; override;
  end;

  TMainForm = class(TfpgForm)
  protected
    FChessboard: TBGRAChessboard;
    FStyle: TBoardStyle;
    FUpsideDown: boolean;
    FGame: TChessGame;
    FUserMove: string;
    FUserColor: TPieceColor;
    FComputerColor: TPieceColorWide;
    FEngine: TFileName;
    FBook: TFileName;
    FEngineLoaded: boolean;
    FMoveHist: TMoveString;
    FPosHist: TStringList;
    FCurrPosIndex: integer;
    FMoveTime: integer;
    FValidator: TFenFilter;
    FDragging: boolean;
    FMousePos, FDragPos, FInitPos: TPoint;
    FPieceIndex: integer;
    FWaitingForComputerMove: boolean;
    FWaitingForAnimation: boolean;
    FWaitingForReadyOk: integer;
    FWaitingForUserMove: boolean;
    FPgnData: TStringList;
    FComputerCastling: boolean;
    FHistoryFilePath: TFileName;
    FXLegend, FYLegend, FXLegendInv, FYLegendInv: TBGRABitmap;
    FPlayingChess960: boolean;
   {FSendMsgGoTime: cardinal;
    FCheckTimeElapsed: boolean;}
    FIsMovePromotion, FIsMoveCapture: boolean;
    procedure HandleKeyPress(var KeyCode: word; var ShiftState: TShiftState; var Consumed: boolean); override;
  public
    destructor Destroy; override;
    procedure AfterCreate; override;
    procedure InitForm;
    procedure WidgetPaint(Sender: TObject);
    procedure TopWidgetPaint(Sender: TObject);
    procedure LeftWidgetPaint(Sender: TObject);
    procedure RightWidgetPaint(Sender: TObject);
    procedure BottomWidgetPaint(Sender: TObject);
    procedure WidgetMouseDown(Sender: TObject; AButton: TMouseButton; AShift: TShiftState; const AMousePos: TPoint);
    procedure WidgetMouseMove(Sender: TObject; AShift: TShiftState; const AMousePos: TPoint);
    procedure WidgetMouseUp(Sender: TObject; AButton: TMouseButton; AShift: TShiftState; const AMousePos: TPoint);
  private
    FChessboardWidget: TfpgWidget;
    FTopLegendWidget: TfpgWidget;
    FLeftLegendWidget: TfpgWidget;
    FRightLegendWidget: TfpgWidget;
    FBottomLegendWidget: TfpgWidget;
    FStatusBar: TfpgPanel;
    FMenuBar: TfpgMenuBar;
    FEschecsSubMenu: TfpgPopupMenu;
    FMovesSubMenu: TfpgPopupMenu;
    FBoardSubMenu: TfpgPopupMenu;
    FOptionsSubMenu: TfpgPopupMenu;
    FTimer: TfpgTimer;
    procedure ItemNewGameClicked(Sender: TObject);
    procedure ItemNewGame960Clicked(Sender: TObject);
    procedure ItemEngineClicked(Sender: TObject);
    procedure OtherItemClicked(Sender: TObject);
    procedure InternalTimerFired(Sender: TObject);
    procedure DoMove(const AMove: string; const APromotion: TPieceTypeWide; const AComputerMove: boolean; out ASkip: boolean);
    procedure OnMoveDone(const AHistory: string; const ASound: boolean = TRUE);
    procedure OnComputerMove;
    procedure OnUserIllegalMove;
    procedure SetComputerColor(const AAutoPlay: boolean);
    procedure NewPosition(const APos: string; const AHistory: string = '');
    function TryNavigate(const ACurrIndex: integer; const ANavig: TNavigation): integer;
    procedure PlaySound(const ASound: integer);
    procedure ItemQuitClicked(Sender: TObject);
    procedure SaveGame(Sender: TObject);
    procedure OnResized(Sender: TObject);
    procedure OnAttributeChanged(Sender: TObject; AWinAttr: TWindowAttributes);
    function LoadFrcPos(const ANumber: integer): string;
    procedure DropPiece(const AMousePos: TPoint; const AAbortMove: boolean = FALSE);
    procedure Send(const ACommand: string);
    function TryCreateConnectedProcess(AEngine: string): boolean;
    function LoadEngine(AEngine: string): boolean;
    function ArbitratorMessage(const ACheck: boolean; const AActive: TPieceColor; const AState: TGameState): string;
  end;

{$I icon}
  
var
  LListener: TThread;
  LLoadedEngineCanPlayChess960: boolean;

procedure TMainForm.Send(const ACommand: string);
begin
  try
    WriteProcessInput(ACommand);
  except
    on E: Exception do
      Log({$I %FILE%} + ' (' + {$I %LINE%} + '): ' + E.Message);
  end;
  Log('-> ' + ACommand);
end;

function TMainForm.TryCreateConnectedProcess(AEngine: string): boolean;
var
  LAbsolutePath: string;
begin
  result := TRUE;
  
  if not FileExists(AEngine) then
  begin
    LAbsolutePath := ExtractFilePath(ParamStr(0)) + AEngine;
    if FileExists(LAbsolutePath) then
    begin
      Log(Format('** %s = %s', [AEngine, LAbsolutePath]));
      AEngine := LAbsolutePath;
    end;
  end;
  
  if FileExists(AEngine) then
  begin
    if MakeFileExecutable(AEngine) then
    begin
      if SetCurrentDir(ExtractFileDir(AEngine)) then
      begin
        if CreateConnectedProcess(ExtractFileName(AEngine)) then
        begin
          Log(Format('** Engine connected [%s]', [AEngine]));
        end else
        begin
          Log('** Cannot create process');
          result := FALSE;
        end;
      end else
      begin
       Log('** Cannot change directory');
       result := FALSE;
      end;
    end else
    begin
      Log('** Cannot make file executable');
      result := FALSE;
    end;
  end else
  begin
    Log(Format('** File not found [%s]', [AEngine]));
    result := FALSE;
  end;
end;

function TMainForm.LoadEngine(AEngine: string): boolean;
const
  CDelay = 200;
begin
  LLoadedEngineCanPlayChess960 := FALSE;
  FBoardSubMenu.MenuItem(1).Enabled := FALSE;
  result := TryCreateConnectedProcess(FEngine);
  if result then
  begin
    Sleep(CDelay);
    Send(MsgUci);
  end;
end;

function TMainForm.ArbitratorMessage(const ACheck: boolean; const AActive: TPieceColor; const AState: TGameState): string;
begin
  case AState of
    gsProgress:
      result := Concat(
        IfThen(ACheck, Concat({$IFDEF USE_LANGUAGE_UNIT}GetText(txCheck){$ELSE}rsCheck{$ENDIF}, ' '), ''),
        IfThen(AActive = pcWhite, {$IFDEF USE_LANGUAGE_UNIT}GetText(txWhiteToMove){$ELSE}rsWhiteToMove{$ENDIF}, {$IFDEF USE_LANGUAGE_UNIT}GetText(txBlackToMove){$ELSE}rsBlackToMove{$ENDIF})
      );
    gsCheckmate:
      result := Concat(
        {$IFDEF USE_LANGUAGE_UNIT}GetText(txCheckmate){$ELSE}rsCheckmate{$ENDIF}, ' ',
        IfThen(AActive = pcWhite, {$IFDEF USE_LANGUAGE_UNIT}GetText(txBlackWins){$ELSE}rsBlackWins{$ENDIF}, {$IFDEF USE_LANGUAGE_UNIT}GetText(txWhiteWins){$ELSE}rsWhiteWins{$ENDIF})
      );
    gsStalemate:
      result := {$IFDEF USE_LANGUAGE_UNIT}GetText(txStalemate){$ELSE}rsStalemate{$ENDIF};
    gsDraw:
      result := {$IFDEF USE_LANGUAGE_UNIT}GetText(txDraw){$ELSE}rsDraw{$ENDIF};
  end;
end;

procedure TMainForm.HandleKeyPress(var KeyCode: word; var ShiftState: TShiftState; var Consumed: boolean);

  procedure ShowFormDebug;
  var
    LForm: TFormDebug;
  begin
    LForm := TFormDebug.Create(Self);
    try
      LForm.ShowModal;
      ActivateWindow;
    finally
      LForm.Free;
    end;
  end;

begin
  {$IFDEF DEBUG_OUTPUT}DebugLn(Format('DEBUG TMainForm.HandleKeyPress(%d)', [KeyCode]));{$ENDIF}
  case KeyCode of
    KeyLeft,
    KeyBackspace: FCurrPosIndex := TryNavigate(FCurrPosIndex, nvPrevious);
    KeyRight:     FCurrPosIndex := TryNavigate(FCurrPosIndex, nvNext);
    KeyUp:        FCurrPosIndex := TryNavigate(FCurrPosIndex, nvLast);
    KeyDown:      FCurrPosIndex := TryNavigate(FCurrPosIndex, nvFirst);
    68: ShowFormDebug; // 'd'
  end;
end;

destructor TMainForm.Destroy;
const
  CDelay = 200;
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG TMainForm.Destroy');{$ENDIF}
  FTimer.Enabled := FALSE;
  if not Assigned(FChessboard) then
  begin
    {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG Destroy procedure already executed');{$ENDIF}
    Exit;
  end;
  FChessboard.Free; FChessboard := nil;
  FGame.Free;
  if FEngineLoaded then
  begin
    Send(MsgQuit);
    Sleep(CDelay);
  end;
  LListener.Terminate;
  LListener.WaitFor;
  LListener.Free;
  FreeConnectedProcess;
  FMoveHist.Free;
  FPosHist.Free;
  FPgnData.Free;
  FValidator.Free;
  FreePictures;
  FXLegend.Free;
  FYLegend.Free;
  FXLegendInv.Free;
  FYLegendInv.Free;
 {FChessboardWidget.Free;
  FTimer.Free;
  FTopLegendWidget.Free;
  FLeftLegendWidget.Free;
  FRightLegendWidget.Free;
  FBottomLegendWidget.Free;}
  inherited Destroy;
end;

procedure TMainForm.AfterCreate;
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG TMainForm.AfterCreate');{$ENDIF}
  Randomize;
  Name := 'MainForm';
 {WindowTitle := 'Eschecs';}
 {SetPosition(0, 0, 9 * 100, 24 + 9 * 100 + 24);}
  BackgroundColor := $80000001;
  IconName := 'vfd.eschecs';
  WindowPosition := wpOneThirdDown{wpAuto}{wpScreenCenter};
{$IFDEF WINDOWS}
  Sizeable := FALSE;
{$ENDIF}
  OnResize := @OnResized;
  OnWindowAttributesChange := @OnAttributeChanged;
  
  FMenuBar := TfpgMenuBar.Create(self);
  with FMenuBar do
  begin
    Name := 'FMenuBar';
    Align := alTop;
    SetPosition(0, 0, 9 * 40, 24);
    Anchors := [anLeft, anRight, anTop];
  end;
  FEschecsSubMenu := TfpgPopupMenu.Create(self);
  with FEschecsSubMenu do Name := 'FEschecsSubMenu';
  FMovesSubMenu   := TfpgPopupMenu.Create(self);
  with FMovesSubMenu   do Name := 'FMovesSubMenu';
  FBoardSubMenu   := TfpgPopupMenu.Create(self);
  with FBoardSubMenu   do Name := 'FBoardSubMenu';
  FOptionsSubMenu := TfpgPopupMenu.Create(self);
  with FOptionsSubMenu do Name := 'FOptionsSubMenu';
  
  FChessboardWidget := TfpgWidget.Create(self);
  with FChessboardWidget do
  begin
    Name := 'FChessboardWidget';
    BackgroundColor := clNone;
    OnPaint := @WidgetPaint;
    OnMouseDown := @WidgetMouseDown;
    OnMouseUp := @WidgetMouseUp;
    OnMouseMove := @WidgetMouseMove;
  end;
  FTopLegendWidget := TfpgWidget.Create(self);
  with FTopLegendWidget do
  begin
    Name := 'FTopLegendWidget';
    BackgroundColor := clNone;
    OnPaint := @TopWidgetPaint;
  end;
  FLeftLegendWidget := TfpgWidget.Create(self);
  with FLeftLegendWidget do
  begin
    Name := 'FLeftLegendWidget';
    BackgroundColor := clNone;
    OnPaint := @LeftWidgetPaint;
  end;
  FRightLegendWidget := TfpgWidget.Create(self);
  with FRightLegendWidget do
  begin
    Name := 'FRightLegendWidget';
    BackgroundColor := clNone;
    OnPaint := @RightWidgetPaint;
  end;
  FBottomLegendWidget := TfpgWidget.Create(self);
  with FBottomLegendWidget do
  begin
    Name := 'FBottomLegendWidget';
    BackgroundColor := clNone;
    OnPaint := @BottomWidgetPaint;
  end;
  FStatusBar := TfpgPanel.Create(self);
  with FStatusBar do
  begin
    Name := 'FStatusBar';
    Align := alBottom;
    Alignment := taLeftJustify;
    BackgroundColor := TfpgColor($FFFFFF);
    FontDesc := '#Label1';
    Style := bsLowered;
    TextColor := TfpgColor($000000);
  end;

  InitForm;
end;

procedure TMainForm.InitForm;
const
  CDefaultTitle = 'Eschecs';
  CMenuBarHeight = 24;
  CDefaultVolume = 25;
  CMaxVolume = 75;
  CEnginesConfigurationFile = {$IFDEF WINDOWS}'windows.ini'{$ELSE}'linux.ini'{$ENDIF};
var
  LCurrPos: string;
  LAuto: boolean;
  LFileName: TFileName;
  LMoveHist: string;
  LLegend: TBGRABitmap;
  LCmdIntf: ICmdLineParams;
  LErr: Tfpgstring;
  LArr: TStringArray;
  LLoadSoundLib: integer;
  LVolume: integer = CDefaultVolume;
  LExpr: TRegExpr;
  LHasOptionPosition: boolean = FALSE;
  LLeft, LTop, LWidth, LHeight: integer;
  s: string;
  i: integer;
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG TMainForm.InitForm');{$ENDIF}
  
  LoadSettings(
    LCurrPos,
    LAuto,
    FUpsideDown,
    FStyle,
    LMoveHist,
    FCurrPosIndex,
    FEngine,
    FBook,
    LLSColor,
    LDSColor,
    LBackColors[bcGreen],
    LBackColors[bcRed],
    LLMColor,
    LLMColor2,
    LDMColor,
    LDMColor2,
    FMoveTime,
    LFont,
   {LLang,}
    LScale,
    FPlayingChess960
  );
  
  if Supports(fpgApplication, ICmdLineParams, LCmdIntf) then
  begin
    if LCmdIntf.ParamCount > 0 then
    begin
      try
       {LErr := LCmdIntf.CheckOptions('a:b:c:f:g:l:m:o:p:r:s:t:u:v:w:', 'autoplay: black: chessboard: font: green: language: marblecolors: openingbook: position: red: size: time: upsidedown: volume: white:');}
        LErr := LCmdIntf.CheckOptions('a:b:c:f:g:m:o:p:r:s:t:u:v:w:', 'autoplay: black: chessboard: font: green: marblecolors: openingbook: position: red: size: time: upsidedown: volume: white:');
      except
        on E: Exception do
          Log('** [CLI] Exception (line ' + {$I %LINE%} + ') [' + E.Message + ']');
      end;
      if Length(LErr) > 0 then
        Log('** [CLI] CheckOptions = ' + LErr)
      else
      begin
        if LCmdIntf.HasOption('o', 'openingbook') then
        begin
          LHasOptionPosition := TRUE;
          s := LCmdIntf.GetOptionValue('o', 'openingbook'); Log('** [CLI] Book = ' + s);
          FBook := s;
        end;
        if LCmdIntf.HasOption('p', 'position') then
        begin
          LHasOptionPosition := TRUE;
          s := LCmdIntf.GetOptionValue('p', 'position'); Log('** [CLI] Position = ' + s);
          LCurrPos := s;
        end;
        if LCmdIntf.HasOption('a', 'autoplay') then
        begin
          s := LCmdIntf.GetOptionValue('a', 'autoplay'); Log('** [CLI] Autoplay = ' + s);
          try
            LAuto := StrToBool(s);
          except
          end;
        end;
        if LCmdIntf.HasOption('u', 'upsidedown') then
        begin
          s := LCmdIntf.GetOptionValue('u', 'upsidedown'); Log('** [CLI] Upsidedown = ' + s);
          try
            FUpsideDown := StrToBool(s);
          except
          end;
        end;
        if LCmdIntf.HasOption('c', 'chessboard') then
        begin
          s := LowerCase(LCmdIntf.GetOptionValue('c', 'chessboard')); Log('** [CLI] Chessboard = ' + s);
          if      s = 'simple'         then FStyle := bsSimple
          else if s = 'marbleoriginal' then FStyle := bsMarbleOriginal
          else if s = 'marblenew'      then FStyle := bsMarbleNew
          else if s = 'marblecustom'   then FStyle := bsMarbleCustom
          else if s = 'marble'         then FStyle := bsMarbleOriginal
          else if s = 'wood'           then FStyle := bsWood;
        end;
        if LCmdIntf.HasOption('t', 'time') then
        begin
          s := LCmdIntf.GetOptionValue('t', 'time'); Log('** [CLI] Time = ' + s);
          FMoveTime := StrToIntDef(s, 999);
        end;
        if LCmdIntf.HasOption('f', 'font') then
        begin
          s := LowerCase(LCmdIntf.GetOptionValue('f', 'font')); Log('** [CLI] Font = ' + s);
          LFont := s;
        end;
        (*
        if LCmdIntf.HasOption('l', 'language') then
        begin
          s := LCmdIntf.GetOptionValue('l', 'language'); Log('** [CLI] Language = ' + s);
          try
            LLang := TLanguage(GetEnumValue(TypeInfo(TLanguage), 'lg' + s));
          except
          end;
        end;
        *)
        if LCmdIntf.HasOption('s', 'size') then
        begin
          s := LCmdIntf.GetOptionValue('s', 'size'); Log('** [CLI] Size = ' + s);
          LScale := StrToIntDef(s, 40);
        end;
        if LCmdIntf.HasOption('w', 'white') then
        begin
          s := UpperCase(LCmdIntf.GetOptionValue('w', 'white')); Log('** [CLI] White = ' + s);
          LExpr := TRegExpr.Create('^[\dA-F]{8}$');
          try          
            if LExpr.Exec(s) then
              LLSColor := StrToBGRA(LExpr.Match[0]);
          except
          end;         
          LExpr.Free;          
        end;
        if LCmdIntf.HasOption('b', 'black') then
        begin
          s := UpperCase(LCmdIntf.GetOptionValue('b', 'black')); Log('** [CLI] Black = ' + s);
          LExpr := TRegExpr.Create('^[\dA-F]{8}$');
          try          
            if LExpr.Exec(s) then
              LDSColor := StrToBGRA(LExpr.Match[0]);
          except
          end;         
          LExpr.Free;          
        end;
        if LCmdIntf.HasOption('g', 'green') then
        begin
          s := UpperCase(LCmdIntf.GetOptionValue('g', 'green')); Log('** [CLI] Green = ' + s);
          LExpr := TRegExpr.Create('^[\dA-F]{8}$');
          try          
            if LExpr.Exec(s) then
              LBackColors[bcGreen] := StrToBGRA(LExpr.Match[0]);
          except
          end;         
          LExpr.Free;          
        end;
        if LCmdIntf.HasOption('r', 'red') then
        begin
          s := UpperCase(LCmdIntf.GetOptionValue('r', 'red')); Log('** [CLI] Red = ' + s);
          LExpr := TRegExpr.Create('^[\dA-F]{8}$');
          try          
            if LExpr.Exec(s) then
              LBackColors[bcRed] := StrToBGRA(LExpr.Match[0]);
          except
          end;         
          LExpr.Free          
        end;
        if LCmdIntf.HasOption('m', 'marblecolors') then
        begin
          s := UpperCase(LCmdIntf.GetOptionValue('m', 'marblecolors')); Log('** [CLI] Marblecolors = ' + s);         
          LExpr := TRegExpr.Create('^([\dA-F]{8}),([\dA-F]{8}),([\dA-F]{8}),([\dA-F]{8})$');
          try          
            if LExpr.Exec(s) then
            begin
              LLMColor := StrToBGRA(LExpr.Match[1]);
              LLMColor2 := StrToBGRA(LExpr.Match[2]);
              LDMColor := StrToBGRA(LExpr.Match[3]);
              LDMColor2 := StrToBGRA(LExpr.Match[4]);
            end;
          except
          end;         
          LExpr.Free;                    
        end;
        if LCmdIntf.HasOption('v', 'volume') then
        begin
          s := LCmdIntf.GetOptionValue('v', 'volume'); Log('** [CLI] Volume = ' + s);
          LVolume := StrToIntDef(s, Pred(CDefaultVolume));
          LVolume := Min(LVolume, CMaxVolume);
          LVolume := Max(LVolume, 0);          
        end;
        try
         {LArr := LCmdIntf.GetNonOptions('a:b:c:f:g:l:m:o:p:r:s:t:u:v:w:', ['autoplay:', 'black:', 'chessboard:', 'font:', 'green:', 'language:', 'marblecolors:', 'openingbook:', 'position:', 'red:', 'size:', 'time:', 'upsidedown:', 'volume:', 'white:']);}
          LArr := LCmdIntf.GetNonOptions('a:b:c:f:g:m:o:p:r:s:t:u:v:w:', ['autoplay:', 'black:', 'chessboard:', 'font:', 'green:', 'marblecolors:', 'openingbook:', 'position:', 'red:', 'size:', 'time:', 'upsidedown:', 'volume:', 'white:']);
        except
          on E: Exception do
            Log('** [CLI] Exception (line ' + {$I %LINE%} + ') [' + E.Message + ']');
        end;
        if Length(LArr) >= 1 then
        begin
          if FileExists(LArr[0]) then
          begin
            FEngine := LArr[0];
            Log('** [CLI] Engine = ' + FEngine);
          end else
            Log('** [CLI] File not found [' + LArr[0] + ']');
          if Length(LArr) >= 1 then
            for i := 1 to High(LArr) do
              Log('** [CLI] Parameter ignored [' + LArr[i] + ']');
        end;
      end;
    end;
  end else
    Log('** [CLI] Cannot process parameters');
  
  FValidator := TFenFilter.Create;
  Assert(FValidator.IsFEN(LCurrPos));
  
  if LHasOptionPosition then
  begin
    SetLength(LMoveHist, 0);
    FCurrPosIndex := 0;
  end else
  Log(Format('** Initializing move history [%s]', [LMoveHist]));
  FMoveHist := TMoveString.Create(LMoveHist);
  
  FHistoryFilePath := Concat(LConfigFilesPath, 'history.fen');
  FPosHist := TStringList.Create;
  if FileExists(FHistoryFilePath) and not LHasOptionPosition then
  begin
    Log(Format('** Loading position history from file [%s]', [FHistoryFilePath]));
    FPosHist.LoadFromFile(FHistoryFilePath);
  end else
    FPosHist.Append(LCurrPos);
  Log(Format('** Move history count = %d', [FMoveHist.GetCount]));
  Log(Format('** Position history count = %d', [FPosHist.Count]));
  
  (*
  // pgnwrite.pas
  procedure WritePgnFile(const APath: TFileName; const AData: TStringList; const AChess960: boolean);
  const
    CWhiteName       = 0;
    CBlackName       = 1;
    CInitialPosition = 2;
    CFirstMove       = 3;
  *)
  
  FPgnData := TStringList.Create;
  FPgnData.Append('?');
  FPgnData.Append('?');
  FPgnData.Append(LCurrPos);
  
  with FMenuBar do
  begin
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txEschecs){$ELSE}rsEschecs{$ENDIF}, nil).SubMenu := FEschecsSubMenu;
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txMoves){$ELSE}rsMoves{$ENDIF},     nil).SubMenu := FMovesSubMenu;
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txBoard){$ELSE}rsBoard{$ENDIF},     nil).SubMenu := FBoardSubMenu;
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txOptions){$ELSE}rsOptions{$ENDIF}, nil).SubMenu := FOptionsSubMenu;
  end;
  with FEschecsSubMenu do
  begin
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txQuit){$ELSE}rsQuit{$ENDIF}, 'Ctrl+Q', @ItemQuitClicked);
    AddMenuItem('-', '', nil);
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txAbout){$ELSE}rsAbout{$ENDIF}, '', @OtherItemClicked);
  end;
  with FOptionsSubMenu do
  begin
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txSound){$ELSE}rsSound{$ENDIF}, '', @OtherItemClicked).Checked := TRUE;
  end;
  with FBoardSubMenu do
  begin
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txNew){$ELSE}rsNew{$ENDIF}, '', @ItemNewGameClicked);
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txNew960){$ELSE}rsNew960{$ENDIF}, '', @ItemNewGame960Clicked).Enabled := FALSE;
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txFlip){$ELSE}rsFlip{$ENDIF}, '', @OtherItemClicked);
  end;
  with FMovesSubMenu do
  begin
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txComputerMove){$ELSE}rsComputerMove{$ENDIF}, '', @OtherItemClicked);
    AddMenuItem({$IFDEF USE_LANGUAGE_UNIT}GetText(txAutoPlay){$ELSE}rsAutoPlay{$ENDIF}, '', @OtherItemClicked).Checked := LAuto;
    AddMenuItem('-', '', nil);
    LFileName := Concat(LConfigFilesPath, CEnginesConfigurationFile);
    if FileExists(LFileName) then
    begin
      LoadEnginesData(LFileName);
      for i := 0 to High(LEngines) do
        with AddMenuItem(LEngines[i].FName, '', @ItemEngineClicked) do
        begin
          with LEngines[i] do
            Enabled := FExists{$IFDEF UNIX} and IsFileExecutable(Concat(FDirectory, FCommand)) or MakeFileExecutable(Concat(FDirectory, FCommand)){$ENDIF};
          Checked := FALSE;
          Tag := i;
        end;
    end;
  end;
  
  LWidth := 9 * LScale;
  LHeight := 24 + 9 * LScale + 24;
  
  if LWidth < fpgApplication.ScreenWidth then LLeft := (fpgApplication.ScreenWidth - LWidth) div 2 else LLeft := 0;
  if LHeight < fpgApplication.ScreenHeight then LTop := (fpgApplication.ScreenHeight - LHeight) div 3 else LTop := 0;
  
  SetPosition(LLeft, LTop, LWidth, LHeight);
  WindowTitle := CDefaultTitle;
  MinWidth := 9 * LScale;
  MinHeight := 24 + 9 * LScale + 24;
  
  FChessboardWidget.SetPosition(0 + LScale div 2, CMenuBarHeight + LScale div 2, 8 * LScale, 8 * LScale);
  FStatusBar.SetPosition(0, 24 + 9 * LScale, 9 * LScale, 24);
  FMenuBar.SetPosition(0, 0, 9 * LScale, 24);
  
  FXLegend := TBGRABitmap.Create(8 * LScale, LScale div 2, ColorToBGRA(clWindowBackground));
  LFileName := Format('%simages/coord/horiz/%d.png', [ExtractFilePath(ParamStr(0)), LScale]);
  if FileExists(LFileName) then
  begin 
    LLegend := TBGRABitmap.Create(LFileName);
    FXLegend.PutImage(0, 0, LLegend, dmDrawWithTransparency);
    LLegend.Free;
  end else
    Log(Format('** File not found [%s]', [LFileName]));
  
  FYLegend := TBGRABitmap.Create(LScale div 2, 8 * LScale, ColorToBGRA(clWindowBackground));
  LFileName := Format('%simages/coord/vert/%d.png', [ExtractFilePath(ParamStr(0)), LScale]);
  if FileExists(LFileName) then
  begin
    LLegend := TBGRABitmap.Create(LFileName);
    FYLegend.PutImage(0, 0, LLegend, dmDrawWithTransparency);
    LLegend.Free;
  end else
    Log(Format('** File not found [%s]', [LFileName]));
  
  FXLegendInv := TBGRABitmap.Create(8 * LScale, LScale div 2, ColorToBGRA(clWindowBackground));
  LFileName := Format('%simages/coord/horiz/inv/%d.png', [ExtractFilePath(ParamStr(0)), LScale]);
  if FileExists(LFileName) then
  begin
    LLegend := TBGRABitmap.Create(LFileName);
    FXLegendInv.PutImage(0, 0, LLegend, dmDrawWithTransparency);
    LLegend.Free;
  end else
    Log(Format('** File not found [%s]', [LFileName]));
  
  FYLegendInv := TBGRABitmap.Create(LScale div 2, 8 * LScale, ColorToBGRA(clWindowBackground));
  LFileName := Format('%simages/coord/vert/inv/%d.png', [ExtractFilePath(ParamStr(0)), LScale]);
  if FileExists(LFileName) then
  begin
    LLegend := TBGRABitmap.Create(LFileName);
    FYLegendInv.PutImage(0, 0, LLegend, dmDrawWithTransparency);
    LLegend.Free;
  end else
    Log(Format('** File not found [%s]', [LFileName]));
  
  FTopLegendWidget.SetPosition(LScale div 2, CMenuBarHeight, 8 * LScale, LScale div 2);
  FLeftLegendWidget.SetPosition(0, CMenuBarHeight + LScale div 2, LScale div 2, 8 * LScale);
  FRightLegendWidget.SetPosition(8 * LScale + LScale div 2, CMenuBarHeight + LScale div 2, LScale div 2, 8 * LScale);
  FBottomLegendWidget.SetPosition(LScale div 2, CMenuBarHeight + 8 * LScale + LScale div 2, 8 * LScale, LScale div 2);
  
  CreatePictures(FStyle, LScale);
  FChessboard := TBGRAChessboard.Create(FStyle, FUpsideDown, LCurrPos);
  FGame := TChessGame.Create(LCurrPos);
  FUserMove := EmptyStr;
  OnMoveDone(FMoveHist.GetString(FCurrPosIndex), FALSE);
  SetComputerColor(FMovesSubMenu.MenuItem(1).Checked);
  
  FTimer := TfpgTimer.Create(10);
  FTimer.OnTimer := @InternalTimerFired;
  
  FWaitingForComputerMove := FALSE;
 {FEngineLoaded := FALSE;}
  FWaitingForAnimation := FALSE;
  FWaitingForReadyOk := 0;
  FWaitingForUserMove := TRUE;
  FComputerCastling := FALSE;
 {FCheckTimeElapsed := FALSE;}
  
  LLoadSoundLib := LoadSoundLib(FALSE); { TRUE = Use system libraries }
  Log(Format('** LoadSoundLib = %d', [LLoadSoundLib])); 
  if LLoadSoundLib >= 0 then
    SetSoundVolume(LVolume)
  else
  begin
    FOptionsSubMenu.MenuItem(0).Checked := FALSE;
    FOptionsSubMenu.MenuItem(0).Enabled := FALSE;
  end;
  
  LListener := TListener.Create(TRUE);
  LListener.Priority := tpHigher;
  LListener.Start;
  
  FEngineLoaded := LoadEngine(FEngine);
  
  FTimer.Enabled := TRUE;
end;

procedure TMainForm.WidgetPaint(Sender: TObject);
begin
  FChessboard.DrawToFPGCanvas(FChessboardWidget.Canvas, 0, 0);
end;

procedure TMainForm.TopWidgetPaint(Sender: TObject);
begin
  if FUpsideDown then
    FXLegendInv.Draw(FTopLegendWidget.Canvas, 0, 0)
  else
    FXLegend.Draw(FTopLegendWidget.Canvas, 0, 0);
end;

procedure TMainForm.LeftWidgetPaint(Sender: TObject);
begin
  if FUpsideDown then
    FYLegendInv.Draw(FLeftLegendWidget.Canvas, 0, 0)
  else
    FYLegend.Draw(FLeftLegendWidget.Canvas, 0, 0);
end;

procedure TMainForm.RightWidgetPaint(Sender: TObject);
begin
  if FUpsideDown then
    FYLegendInv.Draw(FRightLegendWidget.Canvas, 0, 0)
  else
    FYLegend.Draw(FRightLegendWidget.Canvas, 0, 0);
end;

procedure TMainForm.BottomWidgetPaint(Sender: TObject);
begin
  if FUpsideDown then
    FXLegendInv.Draw(FBottomLegendWidget.Canvas, 0, 0)
  else
    FXLegend.Draw(FBottomLegendWidget.Canvas, 0, 0);
end;

procedure TMainForm.WidgetMouseDown(Sender: TObject; AButton: TMouseButton; AShift: TShiftState; const AMousePos: TPoint);
var
  X, Y: integer;
begin
  if (FGame.state = gsProgress) and FWaitingForUserMove then
  begin
    FMousePos := AMousePos;
    FDragPos.X := AMousePos.X mod LScale;
    FDragPos.Y := AMousePos.Y mod LScale;
    FInitPos := AMousePos - FDragPos;
    FChessboard.ScreenToXY(AMousePos, X, Y);
    FPieceIndex := FChessboard.FindPiece(X, Y, TPieceColor(Ord(FGame.ActiveColor)));
    if FPieceIndex > 0 then
    begin
      FUserMove := EncodeSquare(X, Y);
      FDragging := TRUE;
      FChessboard.SavePieceBackground(FInitPos, TRUE);
      FChessboard.ScreenRestore;
    end;
  end;
end;

procedure TMainForm.WidgetMouseMove(Sender: TObject; AShift: TShiftState; const AMousePos: TPoint);
var
  X, Y: integer;
  LMousePos: TPoint;
  LTolerance: integer;
begin
  if FDragging then
  begin
    LMousePos := AMousePos;
    
    LTolerance := LScale div 3;
    if (LMousePos.X - FDragPos.X < 0 - LTolerance)
    or (LMousePos.X - FDragPos.X > 7 * LScale + LTolerance)
    or (LMousePos.Y - FDragPos.Y < 0 - LTolerance)
    or (LMousePos.Y - FDragPos.Y > 7 * LScale + LTolerance) then
    begin
      DropPiece(LMousePos, TRUE);
      Exit;
    end;
    
    FChessboard.RestorePieceBackground(FMousePos - FDragPos);
    FChessboard.SavePieceBackground(LMousePos - FDragPos);
    FChessboard.DrawPiece(LMousePos - FDragPos, FPieceIndex);
    FChessboardWidget.Invalidate;
    FMousePos := LMousePos;
  end else
  begin
    FChessboard.ScreenToXY(AMousePos, X, Y);
    if FWaitingForUserMove and (FChessboard.FindPiece(X, Y, TPieceColor(Ord(FGame.ActiveColor))) > 0) then
      TfpgWidget(Sender).MouseCursor := mcHand
    else
      TfpgWidget(Sender).MouseCursor := mcDefault;
  end;
end;

procedure TMainForm.WidgetMouseUp(Sender: TObject; AButton: TMouseButton; AShift: TShiftState; const AMousePos: TPoint);
begin
  if not FDragging then
    Exit;
  DropPiece(AMousePos);
end;

procedure TMainForm.ItemNewGameClicked(Sender: TObject);
var
  LPos: string;
begin
  FPlayingChess960 := FALSE;
  if LLoadedEngineCanPlayChess960 then
    Send(MsgSetOption('UCI_Chess960', FALSE));
  LPos := CFenStartPosition;
  NewPosition(LPos);
  FMoveHist.Clear;
  FPosHist.Clear;
  FPosHist.Append(LPos);
  FPgnData.Clear;
  FPgnData.Append(LPos);
  FCurrPosIndex := 0;
  
  FWaitingForComputerMove := FALSE;
  FWaitingForAnimation := FALSE;
  FWaitingForReadyOk := 0;
  FWaitingForUserMove := TRUE;
  FComputerCastling := FALSE;
end;

procedure TMainForm.ItemNewGame960Clicked(Sender: TObject);
var
  LPos: string;
begin
  FPlayingChess960 := TRUE;
  Send(MsgSetOption('UCI_Chess960', TRUE));
  LPos := LoadFrcPos(Random(960));
  NewPosition(LPos);
  FMoveHist.Clear;
  FPosHist.Clear;
  FPosHist.Append(LPos);
  FPgnData.Clear;
  FPgnData.Append(LPos);
  FCurrPosIndex := 0;
  
  FWaitingForComputerMove := FALSE;
  FWaitingForAnimation := FALSE;
  FWaitingForReadyOk := 0;
  FWaitingForUserMove := TRUE;
  FComputerCastling := FALSE;
end;

procedure TMainForm.ItemEngineClicked(Sender: TObject);
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG TMainForm.ItemEngineClicked');{$ENDIF}
  if Sender is TfpgMenuItem then
  begin
    if FEngineLoaded then
    begin
      Send(MsgQuit);
      FreeConnectedProcess;
    end;
    
    with LEngines[TfpgMenuItem(Sender).Tag] do
      FEngine := Concat(FDirectory, FCommand);
    
    FEngineLoaded := LoadEngine(FEngine);
  end;
end;

procedure TMainForm.OtherItemClicked(Sender: TObject);

  procedure ShowFormAbout_;
  begin
    ShowFormAbout(
      Concat('Eschecs ', CVersion),
      {$IFDEF USE_LANGUAGE_UNIT}GetText(txAboutMessage){$ELSE}rsAboutMessage{$ENDIF},
      {$IFDEF USE_LANGUAGE_UNIT}GetText(txAbout){$ELSE}rsAbout{$ENDIF},
      'OK',
      'https://gitlab.com/rchastain/eschecs'
    );
  end;

  procedure FlipChessboard;
  begin
    FChessboard.ScreenRestore;
    FChessboard.FlipBoard;
    FChessboardWidget.Invalidate;
    FChessboard.ScreenSave;
    FUpsideDown := FChessboard.UpsideDown;
    FTopLegendWidget.Invalidate;
    FLeftLegendWidget.Invalidate;
    FRightLegendWidget.Invalidate;
    FBottomLegendWidget.Invalidate;
  end;

begin
  if Sender is TfpgMenuItem then
    with TfpgMenuItem(Sender) do
    if Text = {$IFDEF USE_LANGUAGE_UNIT}GetText(txAbout){$ELSE}rsAbout{$ENDIF} then
      ShowFormAbout_
    else
    if Text = {$IFDEF USE_LANGUAGE_UNIT}GetText(txComputerMove){$ELSE}rsComputerMove{$ENDIF} then
      FComputerColor := FGame.ActiveColor
    else
    if Text = {$IFDEF USE_LANGUAGE_UNIT}GetText(txAutoPlay){$ELSE}rsAutoPlay{$ENDIF} then
    begin
      Checked := not Checked;
      SetComputerColor(Checked);
    end else
    if Text = {$IFDEF USE_LANGUAGE_UNIT}GetText(txFlip){$ELSE}rsFlip{$ENDIF} then
      FlipChessboard
    else
    if Text = {$IFDEF USE_LANGUAGE_UNIT}GetText(txSound){$ELSE}rsSound{$ENDIF} then
      Checked := not Checked;
end;

procedure TMainForm.InternalTimerFired(Sender: TObject);
var
  LAnimationTerminated: boolean;
begin
 {if FWaitingForComputerMove and FCheckTimeElapsed then
  begin
    if GetTickCount64 - FSendMsgGoTime > FMoveTime then
    begin
      Send(MsgStop);
      FCheckTimeElapsed := FALSE;
    end;
  end;}
  if FChessboard.Animate(LAnimationTerminated) or FComputerCastling then
    FChessboardWidget.Invalidate
  else
    if FEngineLoaded
    and (FComputerColor = FGame.ActiveColor)
    and (FGame.State = gsProgress)
    and not FWaitingForComputerMove then
    begin
      case FWaitingForReadyOk of
        0:
          begin
            FWaitingForReadyOk := 1;
            Send(MsgPosition(FGame.GetFen(FPlayingChess960)));
            Send(MsgIsReady);
          end;
        1:
          begin
           {FCheckTimeElapsed := FALSE;}
          end;
        2:
          begin
            FWaitingForReadyOk := 0;
            Send(MsgGo(FMoveTime));
            MouseCursor := mcHourGlass;
            FWaitingForComputerMove := TRUE;
            FStatusBar.Text := {$IFDEF USE_LANGUAGE_UNIT}GetText(txWaiting){$ELSE}rsWaiting{$ENDIF};
            FWaitingForUserMove := FALSE;
           {FCheckTimeElapsed := TRUE;
            FSendMsgGoTime := GetTickCount64;}
          end;
      end;
    end;
  if (FWaitingForAnimation and LAnimationTerminated) or FComputerCastling then
  begin
    FWaitingForAnimation := FALSE;
    OnComputerMove;
  end;
  FComputerCastling := FALSE;
end;

procedure TMainForm.DoMove(const AMove: string; const APromotion: TPieceTypeWide; const AComputerMove: boolean; out ASkip: boolean);
const
  CSymbols: array[ptKnight..ptQueen] of char = ('n', 'b', 'r', 'q');
var
  LX, LY: integer;
  LSquare: string;
  LSymbol, LSanMove: string;
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn(Format('DEBUG TMainForm.DoMove(%s,%d,%d)', [AMove, Ord(APromotion), Ord(AComputerMove)]));{$ENDIF}
  ASkip := FALSE;
  if FGame.IsCastling(AMove) then
  begin
    FChessboard.MoveKingRook(AMove, AComputerMove);
    ASkip := TRUE;
    FComputerCastling := AComputerMove;
    FIsMovePromotion := FALSE;
    FIsMoveCapture := FALSE;
  end else
  begin
    LSquare := Copy(AMove, 3, 2);
    DecodeSquare(LSquare, LX, LY);
    if FChessboard.FindPiece(LX, LY) > 0 then
      FChessboard.ErasePiece(LSquare);
    LSquare := FGame.IsEnPassant(AMove);
    if LSquare <> EmptyStr then
      FChessboard.ErasePiece(LSquare);
    if APromotion <> ptNil then
      LSymbol := CSymbols[APromotion]
    else
      LSymbol := EmptyStr;
    if AComputerMove then
      FChessboard.MovePiece(AMove, APromotion);
    LHighlighted := AMove;
    FIsMovePromotion := FGame.IsPromotion(AMove);
    FIsMoveCapture := FGame.IsCapture(AMove);
  end;
  
  LSanMove := FGame.GetSan(AMove);
  
  FGame.DoMove(Concat(AMove, LSymbol));
 {DebugLn(LineEnding + FGame.CurrPosToStr);}
  
  if FGame.Check then
    if FGame.State = gsCheckmate then
      LSanMove := LSanMove + '#'
    else
      LSanMove := LSanMove + '+';
  FPgnData.Append(LSanMove);
  
  FMoveHist.Append(AMove, FCurrPosIndex);
  
  while FPosHist.Count > Succ(FCurrPosIndex) do
    FPosHist.Delete(FPosHist.Count - 1);
  FPosHist.Append(FGame.GetFen);
  
  Inc(FCurrPosIndex);
end;

procedure TMainForm.OnMoveDone(const AHistory: string; const ASound: boolean);
var
  LX, LY: integer;
  LIndex: integer;
  LOpeningName: string;
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG TMainForm.OnMoveDone');{$ENDIF}
  if FGame.Check and FChessboard.ScreenSaved then
  begin
    FGame.GetKingCheckedXY(LX, LY);
    LIndex := FChessboard.FindPiece(LX, LY);
    FChessboard.Highlight(LX, LY, bcRed, LIndex);
    FChessboardWidget.Invalidate;
  end;
  if ASound then
    if FGame.State in [gsCheckmate, gsStalemate, gsDraw] then
      PlaySound(sndEndOfGame)
    else if FGame.Check then
      PlaySound(sndCheck)
    else if FIsMovePromotion then
      PlaySound(sndPromotion)
    else if FIsMoveCapture then
      PlaySound(sndCapture)
    else
      PlaySound(sndMove);
  FStatusBar.Text := ArbitratorMessage(FGame.Check, FGame.ActiveColor, FGame.State);
  if FGame.State in [gsCheckmate, gsStalemate, gsDraw] then
    FStatusBar.BackgroundColor := $FFF692
  else if FGame.Check then
    FStatusBar.BackgroundColor := $FFB3B8
  else
    FStatusBar.BackgroundColor := $FFFFFF;
  if (Length(AHistory) > 0) and not FPlayingChess960 then
  begin
    LOpeningName := GetOpening(AHistory);
    if Length(LOpeningName) > 0 then
      Log(Format('** Found opening name: %s', [LOpeningName]));
  end;
  FWaitingForUserMove := not (FGame.State in [gsCheckmate, gsStalemate, gsDraw]);
end;

procedure TMainForm.OnComputerMove;
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG TMainForm.OnComputerMove');{$ENDIF}
  if not FMovesSubMenu.MenuItem(1).Checked then
    FComputerColor := pcNil;
  MouseCursor := mcHand;
  OnMoveDone(FMoveHist.GetString(FCurrPosIndex));
  FWaitingForComputerMove := FALSE;
  FWaitingForUserMove := TRUE;
end;

procedure TMainForm.OnUserIllegalMove;
begin
  PlaySound(sndIllegal);
end;

procedure TMainForm.SetComputerColor(const AAutoPlay: boolean);
begin
  if AAutoPlay then
    FComputerColor := TPieceColor(1 - Ord(FGame.ActiveColor))
  else
    FComputerColor := pcNil;
end;

procedure TMainForm.NewPosition(const APos: string; const AHistory: string);
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn(Format('DEBUG TMainForm.NewPosition(%s)', [APos]));{$ENDIF}
  FChessboard.Free;
  FChessboard := TBGRAChessboard.Create(FStyle, FUpsideDown, APos);
  FGame.Create(APos);
  OnMoveDone(AHistory, FALSE);
  SetComputerColor(FMovesSubMenu.MenuItem(1).Checked);
  FChessboardWidget.Invalidate;
  FStatusBar.BackgroundColor := $FFFFFF;
end;

function TMainForm.TryNavigate(const ACurrIndex: integer; const ANavig: TNavigation): integer;
begin
  result := ACurrIndex;
  case ANavig of
    nvPrevious:
      if ACurrIndex > 0 then
        result := Pred(ACurrIndex);
    nvNext:
      if ACurrIndex < Pred(FPosHist.Count) then
        result := Succ(ACurrIndex);
    nvLast:
      if ACurrIndex < Pred(FPosHist.Count) then
        result := Pred(FPosHist.Count);
    nvFirst:
      if ACurrIndex > 0 then
        result := 0;
  end;
  if result <> ACurrIndex then
    NewPosition(
      FPosHist[result],
      IfThen(
        result = 0,
        '',
        FMoveHist.GetString(result)
      )
    );
end;

procedure TMainForm.PlaySound(const ASound: integer);
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn(Format('DEBUG TMainForm.PlaySound(%d)', [ASound]));{$ENDIF}
  if FOptionsSubMenu.MenuItem(0).Checked then
    Play(ASound);
end;

procedure TMainForm.ItemQuitClicked(Sender: TObject);
begin
  FTimer.Enabled := FALSE;
  SaveGame(nil);
 {Close;}
 fpgApplication.Terminate;
end;

procedure TMainForm.SaveGame(Sender: TObject);
var
  LMoveHist: string;
begin
  LMoveHist := FMoveHist.GetString;
  SaveSettings(
    FGame.GetFen,
    FMovesSubMenu.MenuItem(1).Checked,
    FUpsideDown,
    FStyle,
    LMoveHist,
    FCurrPosIndex,
    FEngine,
    FBook,
    LLSColor, LDSColor, LBackColors[bcGreen], LBackColors[bcRed],
    LLMColor,
    LLMColor2,
    LDMColor,
    LDMColor2,
    FMoveTime,
    LFont,
   {LLang,}
    LScale,
    FPlayingChess960
  );
  FPosHist.SaveToFile(FHistoryFilePath);
  case FGame.State of
    gsProgress: FPgnData.Append('*');
    gsCheckmate: if FGame.ActiveColor = pcBlack then FPgnData.Append('1-0') else FPgnData.Append('0-1');
    gsStalemate, gsDraw: FPgnData.Append('1/2-1/2');
  end;
  WritePgnFile(Concat(ExtractFilePath(ParamStr(0)), 'game.pgn'), FPgnData, FPlayingChess960);
end;

procedure TMainForm.OnResized(Sender: TObject);
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG TMainForm.OnResized');{$ENDIF}
  FChessboardWidget.Top := (Height -FChessboardWidget.Height) div 2;
  FChessboardWidget.Left := (Width - FChessboardWidget.Width) div 2;
  FTopLegendWidget.Top := FChessboardWidget.Top - FTopLegendWidget.Height;
  FTopLegendWidget.Left := FChessboardWidget.Left;
  FLeftLegendWidget.Top := FChessboardWidget.Top;
  FLeftLegendWidget.Left := FChessboardWidget.Left - FLeftLegendWidget.Width;
  FRightLegendWidget.Top := FChessboardWidget.Top;
  FRightLegendWidget.Left := FChessboardWidget.Left + FChessboardWidget.Width;
  FBottomLegendWidget.Top := FChessboardWidget.Bottom;
  FBottomLegendWidget.Left := FChessboardWidget.Left;
  FChessboardWidget.UpdatePosition;
  FTopLegendWidget.UpdatePosition;
  FLeftLegendWidget.UpdatePosition;
  FRightLegendWidget.UpdatePosition;
  FBottomLegendWidget.UpdatePosition;
end;

procedure TMainForm.OnAttributeChanged(Sender: TObject; AWinAttr: TWindowAttributes);
var
  LAttr: TWindowAttribute;
  LAttrs: TWindowAttributes;
  LStr: string;
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG TMainForm.OnAttributeChanged');{$ENDIF}
  if Assigned(Window) then
  begin
    LStr := EmptyStr;
    LAttrs := Window.WindowAttributes;
    for LAttr in TWindowAttributes do
     {if LAttr in AWinAttr then}
     {if LAttr in Window.WindowAttributes then}
      if LAttr in LAttrs then
      begin
        if Length(LStr) > 0 then
          LStr := LStr + ', ';
        LStr := LStr + GetEnumName(TypeInfo(TWindowAttribute), Ord(LAttr));
      end;
    LStr := '[' + LStr + ']';
    {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG Window attributes = ' + LStr);{$ENDIF}
  end;
 {OnResized(Sender);}
 {DebugLn(Format('%d', [FTop]));}
  UpdatePosition; 
end;

function TMainForm.LoadFrcPos(const ANumber: integer): string;
var
  LFile: TFileName;
begin
  result := CFenStartPosition;
  LFile := Concat(LConfigFilesPath, 'fischerandom.fen');
  if FileExists(LFile) then
    with TStringList.Create do
    try
      LoadFromFile(LFile);
      result := Strings[ANumber];
      Log(Format('** Start position n. %d.', [ANumber]));
    finally
      Free;
    end
  else
    Log(Format('** File not found [%s]', [LFile]));
end;

procedure TMainForm.DropPiece(const AMousePos: TPoint; const AAbortMove: boolean);
var
  LType: TPieceTypeWide;
  X, Y: integer;
  LSkip: boolean;
begin
  {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG TMainForm.DropPiece');{$ENDIF}
  
  FDragging := FALSE;
  FChessboard.ScreenToXY(AMousePos, X, Y);
  FUserMove := Concat(FUserMove, EncodeSquare(X, Y));
  
  if (FUserMove = 'e1g1') and FGame.IsLegal('e1h1') and FGame.IsCastling('e1h1') then begin FUserMove := 'e1h1'; {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG e1g1 = e1h1');{$ENDIF} end;
  if (FUserMove = 'e1c1') and FGame.IsLegal('e1a1') and FGame.IsCastling('e1a1') then begin FUserMove := 'e1a1'; {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG e1c1 = e1a1');{$ENDIF} end;
  if (FUserMove = 'e8g8') and FGame.IsLegal('e8h8') and FGame.IsCastling('e8h8') then begin FUserMove := 'e8h8'; {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG e8g8 = e8h8');{$ENDIF} end;
  if (FUserMove = 'e8c8') and FGame.IsLegal('e8a8') and FGame.IsCastling('e8a8') then begin FUserMove := 'e8a8'; {$IFDEF DEBUG_OUTPUT}DebugLn('DEBUG e8c8 = e8a8');{$ENDIF} end;
  
  if FGame.IsLegal(FUserMove) and not AAbortMove then
  begin
    Log(Format('** User move [%s]', [FUserMove]));
    FChessboard.RestorePieceBackground(FMousePos - FDragPos);
    if FGame.IsPromotion(FUserMove) then
    begin
      FChessboard.SavePieceBackground(FChessboard.XYToScreen(X, Y), TRUE);
      FChessboard.RestorePieceBackground(FChessboard.XYToScreen(X, Y));
      FChessboard.DrawPiece(FChessboard.XYToScreen(X, Y), FPieceIndex);
      FChessboardWidget.Invalidate;
      LType := SelecTPieceTypeWide;
    end else
      LType := ptNil;
    DoMove(FUserMove, LType, FALSE, LSkip);
    if LType <> ptNil then
      FChessboard.SetPieceType(FPieceIndex, LType);
    if not LSkip then
    begin
      FChessboard.SetPieceXY(FPieceIndex, X, Y);
      FChessboard.DrawPiece(FChessboard.XYToScreen(X, Y), FPieceIndex);
      FChessboard.ScreenSave;
      FChessboard.HighlightMove(FUserMove, FPieceIndex);
    end;
    FChessboardWidget.Invalidate;
    OnMoveDone(FMoveHist.GetString(FCurrPosIndex));
  end else
  begin
    FChessboard.RestorePieceBackground(FMousePos - FDragPos);
    FChessboard.DrawPiece(FInitPos, FPieceIndex);
    FChessboardWidget.Invalidate;
    if Copy(FUserMove, 3, 2) <> Copy(FUserMove, 1, 2) then
      OnUserIllegalMove;
  end;
end;

var
  LForm: TMainForm;
  LTruncatedLine: string;
  
procedure TListener.Execute;
const
  CDelay = 50;
begin
  while not Terminated do
  begin
    FMessage := ReadProcessOutput;
    
    if Length(FMessage) > 0 then
{$IFDEF QUEUE_ENGINE_MESSAGE}
      Queue(@OnEngineMessage);
{$ELSE}
      Synchronize(@OnEngineMessage);
{$ENDIF}
    Sleep(CDelay);
  end;
end;

procedure TListener.OnEngineMessage;

procedure CreateIncidentReport(const AMove, AEngine, AFen, AChessboard, AFileName: string);
var
  LList: TStringList;
begin
  LList := TStringList.Create;
  LList.Append(Format('MOVE %s', [AMove]));
  LList.Append(Format('ENGINE %s', [AEngine]));
  LList.Append(Format('FEN %s', [AFen]));
  LList.Append(Format('CHESSBOARD' + LineEnding + '%s', [AChessboard]));
  LList.SaveToFile(AFileName);
  LList.Free;
end;

var
  LName, LAuthor, LMove, LTypeStr, LMove1, LBookMove, LBook, LOptionName, LFileName: string;
  LType: TPieceTypeWide;
  LSkip: boolean;
  LList: TStringList;
  LLineTruncated: boolean;
  i: integer;
begin
  LSkip := FALSE;
  
  {$IFDEF DEBUG_OUTPUT}DebugLn(Format('DEBUG TListener.OnEngineMessage [%s]', [FMessage]));{$ENDIF}
  
  LList := TStringList.Create;
 {ExtractStrings([#10, #13], [' '], PChar(FMessage), LList);}
  LList.Text := FMessage;
  
  if Length(LTruncatedLine) > 0 then
  begin
    LList[0] := Concat(LTruncatedLine, LList[0]);
    Log(Format('** Line repaired [%s]', [LList[0]]));
  end;
  
  LLineTruncated := not (FMessage[High(FMessage)] in [#10, #13]);
  if LLineTruncated then
  begin
    LTruncatedLine := LList[Pred(LList.Count)];
    Log(Format('** Line truncated [%s]', [LTruncatedLine]));
    LList.Delete(Pred(LList.Count));
  end else
    LTruncatedLine := EmptyStr;
  
  for i := 0 to Pred(LList.Count) do
  begin
    if Length(LList[i]) = 0 then
    begin
      Log('** Skipping empty line');
      Continue;
    end;
    
    Log('<- ' + LList[i]);
    
    if IsMsgInfo(LList[i]) then
    begin

    end else
    
    if IsMsgIdName(LList[i], LName) then
    begin
      LForm.WindowTitle := LName;
    end else
    
    if IsMsgIdAuthor(LList[i], LAuthor) then
    begin
      Log(Format('** Received message id author [%s]', [LAuthor]));
    end else
    
    if IsMsgOption(LList[i], LOptionName) then
    begin
      if LOptionName = 'UCI_Chess960' then
      begin
        LLoadedEngineCanPlayChess960 := TRUE;
        LForm.FBoardSubMenu.MenuItem(1).Enabled := TRUE;
      end;
    end else
    
    if IsMsgUciOk(LList[i]) then
    begin
      LForm.Send(MsgNewGame);
    end else
    
    if IsMsgBestMove(LList[i], LMove, LTypeStr) then
    begin
      Log(Format('** Received message bestmove [%s, %s]', [LMove, LTypeStr]));
      
      if Length(LForm.FBook) > 0 then
      begin
        if FileExists(LForm.FBook) then
          LBook := LForm.FBook
        else
          LBook := ExtractFilePath(ParamStr(0)) + LForm.FBook;
        if FileExists(LBook) then
        begin
          LBookMove := BestMove(LForm.FGame.GetFen(LForm.FPlayingChess960), LBook);
          if Length(LBookMove) > 0 then
          begin
            Log(Format('** Found book move [%s]', [LBookMove]));
            LMove := LBookMove;
          end;
        end else
          Log(Format('** Book not found [%s]', [LBook]));
      end;
      
      if not LForm.FPlayingChess960 then
      begin
        LMove1 := EmptyStr;
        if (LMove = 'e1g1') and LForm.FGame.IsLegal('e1h1') and LForm.FGame.IsCastling('e1h1') then begin LMove1 := LMove; LMove := 'e1h1'; end;
        if (LMove = 'e1c1') and LForm.FGame.IsLegal('e1a1') and LForm.FGame.IsCastling('e1a1') then begin LMove1 := LMove; LMove := 'e1a1'; end;
        if (LMove = 'e8g8') and LForm.FGame.IsLegal('e8h8') and LForm.FGame.IsCastling('e8h8') then begin LMove1 := LMove; LMove := 'e8h8'; end;
        if (LMove = 'e8c8') and LForm.FGame.IsLegal('e8a8') and LForm.FGame.IsCastling('e8a8') then begin LMove1 := LMove; LMove := 'e8a8'; end;
        if Length(LMove1) > 0 then
          Log(Format('** %s = %s', [LMove1, LMove]));
      end;
      if LForm.FGame.IsLegal(LMove) then
      begin
        if Length(LTypeStr) = 1 then
          case LTypeStr[1] of
            'n': LType := ptKnight;
            'b': LType := ptBishop;
            'r': LType := ptRook;
            'q': LType := ptQueen;
          end
        else
          LType := ptNil;
        LForm.FChessboard.ScreenRestore;
        LForm.DoMove(LMove, LType, TRUE, LSkip);
      end else
      begin
        ShowMessage(Format({$IFDEF USE_LANGUAGE_UNIT}GetText(txIllegalMove){$ELSE}rsIllegalMove{$ENDIF}, [LMove]));
        Log(Format('** Engine played an illegal move [%s]', [LMove]));
        LFileName := ExtractFilePath(ParamStr(0)) + FormatDateTime('"incident-"yymmddnnss".log"', Now);
        CreateIncidentReport(LMove, LForm.FEngine, LForm.FGame.GetFen(LForm.FPlayingChess960), LForm.FGame.CurrPosToStr, LFileName);
        Log(Format('** Incident report created [%s]', [LFileName]));
        LForm.FMovesSubMenu.MenuItem(1).Checked := FALSE;
        LForm.FComputerColor := pcNil;
        LForm.FStatusBar.Text := LForm.ArbitratorMessage(LForm.FGame.Check, LForm.FGame.ActiveColor, LForm.FGame.State);
      end;
      if not LSkip then
        LForm.FWaitingForAnimation := TRUE;
    end else
    
    if IsMsgReadyOk(LList[i]) then
    begin
      Assert(LForm.FWaitingForReadyOk = 1);
      LForm.FWaitingForReadyOk := 2;
    end else
    
    begin
      Log(Format('** Unrecognized message [%s]', [LList[i]]));
    end;
  end;
  LList.Free;
end;

{$IFDEF REDIRECT_STDERR}
var
  LErrStream: TFileStream;
{$ENDIF}
begin
{
  Redirect StdErr to file.
  https://forum.lazarus.freepascal.org/index.php/topic,54289.msg403585.html#msg403585
  https://forum.lazarus.freepascal.org/index.php/topic,54289.msg403288.html#msg403288
}
{$IFDEF REDIRECT_STDERR}
  LErrStream := TFileStream.Create('eschecs.err', fmOpenReadWrite or fmCreate);
{$IFDEF LINUX}
  FpDup2(LErrStream.Handle, StdErrorHandle);
{$ENDIF}
{$IFDEF WINDOWS}
  AssignStream(StdErr, LErrStream);
  Rewrite(StdErr);
{$ENDIF}
{$ENDIF}
  Log(Format('** Eschecs %s %s %s %s %s FPC %s fpGUI %s BGRABitmap %s', [CVersion, {$I %DATE%}, {$I %TIME%}, {$I %FPCTARGETCPU%}, {$I %FPCTARGETOS%}, {$I %FPCVERSION%}, FPGUI_VERSION, BGRABitmapVersionStr]), TRUE);
  if not DirectoryExists(LConfigFilesPath) then Log(Format('** Directory not found [%s]', [LConfigFilesPath]));
  LTruncatedLine := EmptyStr;
  try
    fpgApplication.Initialize;
    fpgImages.AddMaskedBMP('vfd.eschecs', @eschecs_icon, SizeOf(eschecs_icon), 0, 0);
{$IFDEF USE_STYLE}
    if fpgStyleManager.SetStyle('eschecs') then
      fpgStyle := fpgStyleManager.Style;
{$ENDIF}
    fpgApplication.CreateForm(TMainForm, LForm);
    fpgApplication.MainForm := LForm;
    LForm.Show;
    fpgApplication.Run;
    FreeUos;
    LForm.Free;
    fpgApplication.Terminate;
  except
    on E: Exception do
      Log(E.ClassName + ': ' + E.Message);
  end;
{$IFDEF REDIRECT_STDERR}
  LErrStream.Free;
{$ENDIF}
end.
